<?php
/**
 * @package     Error.php
 * @author      Jing <tangjing3321@gmail.com>
 * @link        http://www.slimphp.net
 * @version     1.0
 * @copyright   Copyright (c) SlimCustom.
 * @date        2017年6月5日
 */

namespace SlimCustom\Libs\Handlers;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Http\Body;
use UnexpectedValueException;
use Slim\Handlers\AbstractError;

/**
 * Default SlimCustom application error handler
 *
 * It outputs the error message and diagnostic information in either JSON, XML,
 * or HTML based on the Accept header.
 */
class Error extends AbstractError
{

    /**
     * Invoke error handler
     *
     * @param ServerRequestInterface $request
     *            The most recent Request object
     * @param ResponseInterface $response
     *            The most recent Response object
     * @param \Exception $exception
     *            The caught Exception object
     *            
     * @return ResponseInterface
     * @throws UnexpectedValueException
     */
    public function __invoke(ServerRequestInterface $request, ResponseInterface $response, \Exception $exception)
    {
        $contentType = response()->determineContentType();
        switch ($contentType) {
            case 'application/json':
                $output = $this->renderJsonErrorMessage($exception);
                break;
            
            case 'text/xml':
            case 'application/xml':
                $output = $this->renderXmlErrorMessage($exception);
                break;
            
            case 'text/html':
                $output = $this->renderHtmlErrorMessage($exception);
                break;
            
            default:
                throw new UnexpectedValueException('Cannot render unknown content type ' . $contentType);
        }
        
        $this->writeToErrorLog($exception);
        
        $body = new Body(fopen('php://temp', 'r+'));
        $body->write($output);
        
        return $response->withStatus(500)
            ->withHeader('Content-type', $contentType)
            ->withBody($body);
    }

    /**
     * Render HTML error page
     *
     * @param \Exception $exception            
     *
     * @return string
     */
    protected function renderHtmlErrorMessage(\Exception $exception)
    {
        $title = 'SlimCustom Application Error';
        
        if ($this->displayErrorDetails) {
            $html = '<p>The application could not run because of the following error:</p>';
            $html .= '<h2>Details</h2>';
            $html .= $this->renderHtmlException($exception);
            
            while ($exception = $exception->getPrevious()) {
                $html .= '<h2>Previous exception</h2>';
                $html .= $this->renderHtmlExceptionOrError($exception);
            }
        } else {
            $html = '<p>A website error has occurred. Sorry for the temporary inconvenience.</p>';
        }
        
        $output = sprintf("<html><head><meta http-equiv='Content-Type' content='text/html; charset=utf-8'>" . "<title>%s</title><style>body{margin:0;padding:30px;font:12px/1.5 Helvetica,Arial,Verdana," . "sans-serif;}h1{margin:0;font-size:48px;font-weight:normal;line-height:48px;}strong{" . "display:inline-block;width:65px;}</style></head><body><h1>%s</h1>%s</body></html>", $title, $title, $html);
        
        return $output;
    }

    /**
     * Render exception as HTML.
     *
     * Provided for backwards compatibility; use renderHtmlExceptionOrError().
     *
     * @param \Exception $exception            
     *
     * @return string
     */
    protected function renderHtmlException(\Exception $exception)
    {
        return $this->renderHtmlExceptionOrError($exception);
    }

    /**
     * Render exception or error as HTML.
     *
     * @param \Exception|\Error $exception            
     *
     * @return string
     */
    protected function renderHtmlExceptionOrError($exception)
    {
        if (! $exception instanceof \Exception && ! $exception instanceof \Error) {
            throw new \RuntimeException("Unexpected type. Expected Exception or Error.");
        }
        
        $html = sprintf('<div><strong>Type:</strong> %s</div>', get_class($exception));
        
        if (($code = $exception->getCode())) {
            $html .= sprintf('<div><strong>Code:</strong> %s</div>', $code);
        }
        
        if (($message = $exception->getMessage())) {
            $html .= sprintf('<div><strong>Message:</strong> %s</div>', htmlentities($message));
        }
        
        if (($file = $exception->getFile())) {
            $html .= sprintf('<div><strong>File:</strong> %s</div>', $file);
        }
        
        if (($line = $exception->getLine())) {
            $html .= sprintf('<div><strong>Line:</strong> %s</div>', $line);
        }
        
        if (($trace = $exception->getTraceAsString())) {
            $html .= '<h2>Trace</h2>';
            $html .= sprintf('<pre>%s</pre>', htmlentities($trace));
        }
        
        return $html;
    }

    /**
     * Render JSON error
     *
     * @param \Exception $exception            
     *
     * @return string
     */
    protected function renderJsonErrorMessage(\Exception $exception)
    {
        $error = [
            'message' => 'SlimCustom Application Error'
        ];
        
        if ($this->displayErrorDetails) {
            $error['exception'] = [];
            
            do {
                $error['exception'][] = [
                    'type' => get_class($exception),
                    'code' => $exception->getCode(),
                    'message' => $exception->getMessage(),
                    'file' => $exception->getFile(),
                    'line' => $exception->getLine(),
                    'trace' => explode("\n", $exception->getTraceAsString())
                ];
            } while ($exception = $exception->getPrevious());
        }
        
        return json_encode($error, JSON_PRETTY_PRINT);
    }

    /**
     * Render XML error
     *
     * @param \Exception $exception            
     *
     * @return string
     */
    protected function renderXmlErrorMessage(\Exception $exception)
    {
        $xml = "<error>\n  <message>SlimCustom Application Error</message>\n";
        if ($this->displayErrorDetails) {
            do {
                $xml .= "  <exception>\n";
                $xml .= "    <type>" . get_class($exception) . "</type>\n";
                $xml .= "    <code>" . $exception->getCode() . "</code>\n";
                $xml .= "    <message>" . $this->createCdataSection($exception->getMessage()) . "</message>\n";
                $xml .= "    <file>" . $exception->getFile() . "</file>\n";
                $xml .= "    <line>" . $exception->getLine() . "</line>\n";
                $xml .= "    <trace>" . $this->createCdataSection($exception->getTraceAsString()) . "</trace>\n";
                $xml .= "  </exception>\n";
            } while ($exception = $exception->getPrevious());
        }
        $xml .= "</error>";
        
        return $xml;
    }

    /**
     * Returns a CDATA section with the given content.
     *
     * @param string $content            
     * @return string
     */
    private function createCdataSection($content)
    {
        return sprintf('<![CDATA[%s]]>', str_replace(']]>', ']]]]><![CDATA[>', $content));
    }
    
    /**
     * Wraps the error_log function so that this can be easily tested
     *
     * @param $message
     */
    protected function logError($message)
    {
        error_log($message . PHP_EOL, 3, \SlimCustom\Libs\App::$instance->dataPath() . 'logs/Error_' . date('Ymd') . '.log');
    }
}
